// Copyright (c) 2002 Cunningham & Cunningham, Inc.
// Released under the terms of the GNU General Public License version 2 or later.

package com.fit4as.fit
{

import com.fit4as.util.iterator.ArrayIterator;
import com.fit4as.util.iterator.Iterator;


public class RowFixture extends ColumnFixture {

    public var results : Array;
    public var missing : Array = new Array();
    public var surplus : Array = new Array();


    override public function doRows(rows : Parse) : void {
        try {
            bind(rows.parts);
            results = query();
            match(list(rows.more), list(results), 0);

            var last : Parse = rows.last();
            last.more = buildRows(surplus);
            mark(last.more, "surplus");
            mark(new ArrayIterator(missing), "missing");

        } catch (e : Exception) {
            exception(rows.leaf(), e);
        }
    }

    // abstract
    // get rows to be compared
    public function query() : Array {
        throw new Exception("abstract");
        return null;
    }

    protected function match(expected : Array, computed : Array, col : int) : void {
        if (col >= columnBindings.length) {
            check(expected, computed);
        } else if (columnBindings[col] == null) {
            match(expected, computed, col + 1);
        } else {
            var eMap : HashMap = eSort(expected, col);
            var cMap : HashMap = cSort(computed, col);
            var keys : Array = unionOfPrimitives(eMap.keySet(), cMap.keySet());
            //t race( "match " + keys );
            for each (var key : Object in keys) {
                var eList : Array = eMap.get(key) as Array;
                var cList : Array = cMap.get(key) as Array;
                if (eList == null) {
                    surplus = surplus.concat(cList);
                } else if (cList == null) {
                    missing = missing.concat(eList);
                } else if (eList.length == 1 && cList.length == 1) {
                    check(eList, cList);
                } else {
                    match(eList, cList, col + 1);
                }
            }
        }
    }

    protected function list(rows : *) : Array {
        var result : Array = new Array();
        if (rows is Parse)
        {
            while (rows != null) {
                result.push(rows);
                rows = rows.more;
            }
        }
        else if (rows is Array)
        {
            for (var i : int = 0; i < rows.length; i++) {
                result.push(rows[i]);
            }
        }
        return result;
    }


    protected function eSort(list : Array, col: int) : HashMap {
        var a : TypeAdapter = columnBindings[col];
        var result : HashMap = new HashMap();
        for each(var row : Parse in list) {
            var cell : Parse = row.parts.at(col);
            try {
                var key : Object = a.parse(cell.text());
                bin(result, key, row);
            } catch (e : Exception) {
                exception(cell, e);
                for (var rest : Parse = cell.more; rest != null; rest = rest.more) {
                    ignore(rest);
                }
            }
        }
        return result;
    }

    protected function cSort(list : Array, col : int) : HashMap {
        var a : TypeAdapter = columnBindings[col];
        var result : HashMap = new HashMap();
        for each (var row : Object in list) {
            try {
                a.target = row;
                var key : Object = a.get();
                bin(result, key, row);
            } catch (e : Exception) {
                // surplus anything with bad keys, including null
                surplus.push(row);
            }
        }
        return result;
    }

    protected function bin(map : HashMap, key : Object, row: Object) : void {
        //TODO What else can be the key? Do we need to handle Objects as well?
        //Do we have to implement something like equals and hashcode?
        if (key is Array) {
            key = key.toString();
        }
        if (map.containsKey(key)) {
            map.get(key).push(row);
        } else {
            var list : Array = new Array();
            list.push(row);
            map.put(key, list);
        }
    }

    protected function unionOfPrimitives(a : Array, b : Array) : Array {
        var result : Array = new Array();
        var item : Object;
        for each(item in a)
        {
            if (result.indexOf(item) == -1) result.push(item);
        }
        for each(item in b)
        {
            if (result.indexOf(item) == -1) result.push(item);
        }
        return result;
    }

    override public function check(... rest) : void {
        if (! ( rest.length == 2 && rest[ 0 ] is Array && rest[ 1 ] is Array ))
        {
            super.check.apply(this, rest);
            return;
        }
        var eList : Array = rest[ 0 ];
        var cList : Array = rest[ 1 ];

        if (eList.length == 0) {
            surplus = surplus.concat(cList);
            return;
        }
        if (cList.length == 0) {
            missing = missing.concat(eList);
            return;
        }
        var row : Parse = Parse(eList.shift());
        var cell : Parse = row.parts;
        var obj : Object = cList.shift();
        for (var i: int = 0; i < columnBindings.length && cell != null; i++) {
            var a : TypeAdapter = columnBindings[i];
            if (a != null) {
                a.target = obj;
            }
            super.check(cell, a);
            cell = cell.more;
        }
        check(eList, cList);
    }

    protected function mark(rows : *, message : String) : void {
        var annotation : String = label(message);
        if (rows is Parse)
        {
            while (rows != null) {
                wrong(rows.parts);
                rows.parts.addToBody(annotation);
                rows = rows.more;
            }
        }
        else if (rows is Iterator)
        {
            while (rows.hasNext()) {
                ;
                var row : Parse = Parse(rows.next());
                wrong(row.parts);
                row.parts.addToBody(annotation);
            }
        }
    }

    protected function buildRows(rows : Array) : Parse {
        var root : Parse = Parse.createParseFromStrings( null, null, null, null );
        var next : Parse = root;
        for (var i : int = 0; i < rows.length; i++) {
            next = next.more = Parse.createParseFromStrings("tr", null, buildCells(rows[i]), null);
        }
        return root.more;
    }

    protected function buildCells(row : Object) : Parse {
        if (row == null) {
            var nil : Parse = Parse.createParseFromStrings("td", "null", null, null);
            nil.addToTag(" colspan=" + columnBindings.length);
            return nil;
        }
        var root : Parse = Parse.createParseFromStrings(null, null, null, null);
        var next : Parse = root;
        for (var i : int = 0; i < columnBindings.length; i++) {
            next = next.more = Parse.createParseFromStrings("td", "&nbsp;", null, null);
            var a : TypeAdapter = columnBindings[i];
            if (a == null) {
                ignore(next);
            } else {
                try {
                    a.target = row;
                    info(next, a.toString(a.get()));
                } catch (e : Exception) {
                    exception(next, e);
                }
            }
        }
        return root.more;
    }
}
}